Skip to content

feat: add reasoning token support to OpenRouter integration#3264

Merged
julian-risch merged 6 commits into
deepset-ai:mainfrom
ArkaD171717:openrouter-reasoning-support
Jun 8, 2026
Merged

feat: add reasoning token support to OpenRouter integration#3264
julian-risch merged 6 commits into
deepset-ai:mainfrom
ArkaD171717:openrouter-reasoning-support

Conversation

@ArkaD171717

@ArkaD171717 ArkaD171717 commented May 2, 2026

Copy link
Copy Markdown
Contributor

Related Issues

Proposed Changes:

OpenRouter returns reasoning/thinking content as extra fields on the completion response (reasoning / reasoning_details), which the parent OpenAIChatGenerator doesn't know about, so they were silently dropped. This adds reasoning support to OpenRouterChatGenerator:

  • Overrides run() / run_async() to convert completions with a dedicated _convert_openrouter_completion_to_chat_message, which reads the reasoning fields via getattr (OpenRouter attaches them as extra attributes on standard OpenAI SDK models, not typed fields) and attaches them to ChatMessage as ReasoningContent.
  • Reasoning is captured for non-streaming requests only. Streaming reuses the parent's stream handlers (no reasoning extraction); when streaming is combined with a reasoning generation kwarg, the component logs a warning pointing the user to non-streaming mode. This mirrors MistralChatGenerator.
  • Multi-turn: _prepare_api_call re-injects reasoning_details into the formatted message dicts before sending them back to the API, since to_openai_dict_format() strips reasoning.
  • run() / run_async() now accept list[ChatMessage] | str and normalize via _normalize_messages, matching the parent. Requires haystack-ai>=2.30.0.

How did you test it?

  • Added unit tests covering reasoning extraction, sync/async runs, multi-turn re-injection, string input, the streaming-with-reasoning warning, and edge cases (empty messages, logprobs, malformed tool-call JSON).
  • Added a live test_live_run_with_reasoning integration test, verified against deepseek/deepseek-r1.

Notes for the reviewer

  • Reasoning content is intentionally not captured during streaming: the parent's streaming handlers are reused and the component warns when streaming is combined with reasoning. This keeps the integration aligned with MistralChatGenerator and avoids duplicating the parent's streaming code.

Checklist

Extract reasoning/thinking content from OpenRouter responses and attach
to ChatMessage via ReasoningContent. Override run(), run_async(), and
stream handler. Handle multi-turn by re-injecting reasoning_details
into formatted message dicts.

13 new tests covering extraction, conversion, streaming, multi-turn,
edge cases, and async paths.

Closes deepset-ai#2181
@ArkaD171717 ArkaD171717 requested a review from a team as a code owner May 2, 2026 09:30
@ArkaD171717 ArkaD171717 requested review from julian-risch and removed request for a team May 2, 2026 09:30
@CLAassistant

CLAassistant commented May 2, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Heads-up for maintainers

This PR is from a fork and touches integrations whose integration tests require API keys.
Those tests are skipped in CI because fork PRs don't have access to repo secrets for security reasons.

Affected integrations:

  • openrouter

Please run the integration tests locally (hatch run test:integration inside each folder) before approving.

@github-actions github-actions Bot added the type:documentation Improvements or additions to documentation label May 2, 2026
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Coverage report (openrouter)

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  integrations/openrouter/src/haystack_integrations/components/generators/openrouter/chat
  chat_generator.py 77, 389, 445-447, 464
Project Total  

This report was generated by python-coverage-comment-action

ArkaD171717 and others added 4 commits May 2, 2026 13:19
Add tests for empty messages, logprobs, malformed tool call JSON,
empty streaming choices, async empty messages, and async streaming
with reasoning content.
logprobs Choice requires top_logprobs field, was missing.
Inline imports in test methods trigger PLC0415 lint error.
@ArkaD171717

Copy link
Copy Markdown
Contributor Author

Brought up to date

@ArkaD171717

Copy link
Copy Markdown
Contributor Author

@julian-risch could you take a look at this PR when you get the time please?

- Accept `list[ChatMessage] | str` in run/run_async and normalize via
  `_normalize_messages`. Fixes the mypy override (LSP) error and restores the
  string-input support that OpenAIChatGenerator now provides.
- Stop overriding the streaming handlers and the chunk converter; inherit them
  from the parent and instead warn when streaming is combined with reasoning,
  since reasoning content can only be reconstructed from non-streaming
  responses (mirrors MistralChatGenerator). This removes ~150 lines of
  duplicated parent code that also silently dropped `reasoning_details` during
  streaming, so multi-turn reasoning pass-back no longer no-ops when streaming.
- Restore the explanatory comments that were dropped from `to_dict` and
  `_prepare_api_call` (the `openai_endpoint` hint mechanism and the `to_dict`
  override rationale).
- Bump the `haystack-ai` dependency to >=2.30.0 for `_normalize_messages`.
- Tests: add a live `test_live_run_with_reasoning` integration test (verified
  against deepseek/deepseek-r1) plus string-input and
  streaming-with-reasoning-warning unit tests; drop the tests for the removed
  streaming converter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@julian-risch julian-risch force-pushed the openrouter-reasoning-support branch from 6c2627b to 5664c24 Compare June 7, 2026 13:50
@julian-risch julian-risch removed the type:documentation Improvements or additions to documentation label Jun 7, 2026

@julian-risch julian-risch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for opening this PR @ArkaD171717! It's ready to be merged now. Congratulations on your first contribution to Haystack!

Before merging I pushed a commit with a few adjustments:

  • Fixed CI failures: run/run_async now accept list[ChatMessage] | str and call _normalize_messages. This was a recent change in haystack after you opened the PR. I therefore bumped the dependency to haystack-ai>=2.30.0 too.

  • Streaming reasoning: removed the overridden stream handlers/chunk converter (which silently dropped reasoning_details during streaming). streaming now inherits the parent handlers and warns when combined with reasoning, capturing reasoning for non-streaming only. This mirrors MistralChatGenerator and cuts ~150 lines of duplicated parent code. This consistency with MistralChatGenerator makes it easier for us to maintain the code base.

  • Restored the explanatory comments dropped from to_dict/_prepare_api_call. Please keep changes to unrelated comments in any future PRs as limited as possible.

  • Added a live test_live_run_with_reasoning integration test because we shouldn't rely only on unit tests for this new feature. I can confirm that the integration test passed on my local machine.

@ArkaD171717

Copy link
Copy Markdown
Contributor Author

Thank you! Edits look good to me.

@julian-risch julian-risch merged commit 70d9b5b into deepset-ai:main Jun 8, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenRouter - reasoning support

3 participants